iT邦幫忙

2024 iThome 鐵人賽

DAY 27
0
Software Development

DDD? Clean Architecture? Microservices? 帶你用.NET實作打造一個現代化微服務!系列 第 27

Day 27 - Backend 的最後一哩路:Gateway 與 GraphQL Stitching

  • 分享至 

  • xImage
  •  

前言

在上一篇文章中,我們完成了 AccountTodo 的 GraphQL 服務。透過這兩個服務,我們可以輕鬆地讀取 UserTodo ListTodo Item 的資料。然而,這僅僅是基礎功能,今天我們要深入了解 GraphQL 的核心功能之一——Stitching。我們將使用 GraphQL Gateway 將 UserTodo ListTodo Item 整合起來,並且將這些服務統一在 BFF (Backend for Frontend) Gateway 內,使得所有查詢都可以通過一個單一入口點完成。

創立專案

首先,我們需要在 BFF 資料夾中創建一個名為 GraphQL.Gateway 的新專案。這個專案將使用 HotChocolate.Stitching 來整合我們之前實作的各個 GraphQL 服務。

## Todo GraphQL Project
cd .\BFF\
dotnet new web -n GraphQL.Gateway
cd .\GraphQL.Gateway\
dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Stitching
cd ..\..\

## Add Projects to Solution
dotnet sln add (ls -r **/*.csproj)

Program.cs

Program.cs 中,我們需要註冊並配置 HotChocolate 的服務,以實現遠端 GraphQL schema 的合併,進而達成 stitching 的功能。我們將為 AccountTodo 服務配置對應的 HttpClient,並將它們的 schema 整合到 GraphQL Gateway 中。特別地,我們設定 ignoreRootTypestrue,這樣可以忽略遠端服務的根類型,以避免因為多個類型命名為 Query 而造成的衝突,這也讓我們更好地控制我們希望對外暴露的 GraphQL 結構。

using HotChocolate.AspNetCore;

const string ACCOUNT = "account";
const string TODO = "todo";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient(ACCOUNT, c => c.BaseAddress = new Uri("http://localhost:5068/graphql"));
builder.Services.AddHttpClient(TODO, c => c.BaseAddress = new Uri("http://localhost:5261/graphql"));
// 註冊 GraphQL 服務,並將遠端 schema 加入到當前 Gateway 中
builder.Services.AddGraphQLServer()
    .AddQueryType(d => d.Name("Query"))
    .AddRemoteSchema(ACCOUNT, ignoreRootTypes: true)
    .AddRemoteSchema(TODO, ignoreRootTypes: true)
    .AddTypeExtensionsFromFile("./Stitching.graphql");

var app = builder.Build();

app.MapGraphQL();

app.Run();

在這段程式碼中,AddTypeExtensionsFromFile("./Stitching.graphql") 是告訴 HotChocolate 從指定檔案中載入自定義的 stitching 擴展配置,以便整合不同的 GraphQL schema。

Stitching 設定

接下來,我們創建一個 Stitching.graphql 文件,定義如何將 AccountTodo 兩個 GraphQL schema stitch 在一起,讓不同服務之間的資料可以關聯起來。

在 GraphQL 中,extend type 允許我們擴展現有的類型,添加新的欄位或功能。在這裡,由於我們在之前的服務中設定了 ignoreRootTypes: true,原先的 Query 將會變成空的,因此我們需要使用 extend type Query 來定義所有 GraphQL Gateway 可見的查詢方法。

# 擴展 Query,這裡的方法會成為所有 GraphQL Gateway 會看到的 Queries
extend type Query {
  userById(userId: String!): UserDto @delegate(schema: "account")
  todoListById(listId: String!): TodoListDto @delegate(schema: "todo")
  todoItemById(itemId: String!): TodoItemDto @delegate(schema: "todo")
}

# 擴展 UserDto 型別,讓 User 與 Todo List 產生關係,並透過 @delegate 將資料 Stitch 連接
extend type UserDto {
  todoListDtos: [TodoListDto!]
    @delegate(schema: "todo", path: "todoListsByUserId(userId: $fields:id)")

# 擴展 TodoListDto 型別,讓 Todo List 與 Todo Item 產生關係,並透過 @delegate 將資料 Stitch 連接
extend type TodoListDto {
  todoItemDtos: [TodoItemDto!]
    @delegate(schema: "todo", path: "todoItemsByListId(listId: $fields:id)")
}

# 擴展 TodoItemDto 型別,讓 Todo Item 與 Todo List 產生關係,並透過 @delegate 將資料 Stitch 連接
extend type TodoItemDto {
  todoListDto: TodoListDto @delegate(schema: "todo")
}

這樣的設定確保了 UserDtoTodoListDtoTodoItemDto 之間的資料關聯,使得我們可以通過一個查詢獲取完整的資料結構,實現了 GraphQL 的強大功能。

而我們這裡只做了使用 ID 搜尋的 Query 功能,原因之一是因為這種 Stitch 的查詢如果沒有優化,資料量大的話效能會很差。其二,是因為我們的前端也不需要太彈性的查詢功能。

測試 - GraphQL Gateway

執行

先跑起所有的 GraphQL 專案。

結果

查詢看看我們新增加的 Stitch 功能。

https://ithelp.ithome.com.tw/upload/images/20241010/20168953KpjqjRqIEd.png

所有關聯資料都被我用一個 Query 就拿回來了,而且我還可以隨意的增減我想要的屬性,是不是開始感受到 GrahpQL 的威力了!

加入到 BFF Gateway

雖然 GraphQL 本質上是通過 HTTP 進行連線,但將 REST Gateway 整合 GraphQL 有一些顯著的優點:

  1. 統一資料入口:BFF Gateway 可以通過 GraphQL 將多個後端服務整合到單一入口,簡化前端與後端的溝通。

  2. 靈活查詢:GraphQL 允許前端自定義查詢所需的資料,減少多次請求與過度查詢的情況,提升效能。

  3. 減少 API 版本管理:GraphQL 的靈活性使後端資料結構變更時,無需頻繁更新 API 版本。

  4. 簡化前端邏輯:前端僅需針對統一的 GraphQL API,降低處理多個後端服務的複雜性。

  5. 提升開發與維護效率:GraphQL 的靈活查詢和自動文件化的特性,讓開發團隊更容易探索後端 API 並保持 API 使用的一致性。

透過整合 GraphQL,REST Gateway 可以提升效能、簡化開發流程,並且為系統擴展性提供更好的基礎。

安裝相關套件

在 BFF.Gateway 專案底下新增套件。

dotnet add package GraphQL.Client
dotnet add package GraphQL.Client.Serializer.SystemTextJson

加入 GraphQL Client

我們在 Program.cs 內加入下面的程式碼。

using GraphQL.Client.Abstractions;
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.SystemTextJson;

//...

// Register GraphQLHttpClient for DI
builder.Services.AddSingleton<IGraphQLClient>(s =>
{
    var options = new GraphQLHttpClientOptions
    {
        EndPoint = new Uri("http://localhost:5214/graphql/")
    };

    return new GraphQLHttpClient(options, new SystemTextJsonSerializer());
});

加入 GraphQL Controller

然後新增一個 GraphQLController,方便起見,這就不加 Authorize 了。

using GraphQL;
using GraphQL.Client.Abstractions;
using Microsoft.AspNetCore.Mvc;

namespace BFF.Gateway.Controllers;

[ApiController]
[Route("api/[controller]")]
public class GraphQLController : ControllerBase
{
    private readonly IGraphQLClient _graphQLClient;

    public GraphQLController(IGraphQLClient graphQLClient)
    {
        _graphQLClient = graphQLClient;
    }

    // Define a model to accept GraphQL query and variables from the POST body
    public class GraphQLQuery
    {
        public required string Query { get; set; }
        public object? Variables { get; set; } = new { };
    }

    [HttpPost("execute")]
    public async Task<IActionResult> ExecuteQueryAsync([FromBody] GraphQLQuery request)
    {
        var graphQLRequest = new GraphQLRequest
        {
            Query = request.Query,
            Variables = request.Variables
        };

        try
        {
            var response = await _graphQLClient.SendQueryAsync<dynamic>(graphQLRequest);

            if (response.Errors != null && response.Errors.Length > 0)
            {
                return BadRequest(response.Errors);
            }

            return Ok(response.Data);
        }
        catch (Exception ex)
        {
            return StatusCode(500, $"Internal server error: {ex.Message}");
        }
    }
}

測試 GraphQL Query on BFF

我們把先前在 GraphQL UI 上的 GraphQL Query 和 GraphQL Variables 貼到這個網址:

GraphQL to JSON Body Converter

就可以得到 JSON Body,再貼回 .http 上執行。

### GraphQLController - Query
POST {{HostAddress}}/api/GraphQL/query
Content-Type: application/json

{
  "query": "query ($userId: String!) { userById(userId: $userId) { id firstName lastName email todoListDtos { id name description todoItemDtos { id content state color } } }}",
  "variables": {
    "userId": "1C8EFD0D-05AC-427E-86DB-F13E4BB6F813"
  }
}

https://ithelp.ithome.com.tw/upload/images/20241010/20168953WxilsegVNP.png

Allow CORS

到此,我們的 Backend 部分到了一段落,為了接下來與前端的對接,我們需要讓我們的 BFF Gateway 允許 CORS。

CORS (跨來源資源共享) 是一種瀏覽器安全機制,其核心是由服務器端來配置,並且瀏覽器根據這些配置來執行安全策略,允許網頁從不同來源的伺服器請求資源。它通過添加 HTTP 標頭來控制哪些來源能夠訪問特定的伺服器資源,從而保護用戶免受跨站點請求偽造 (CSRF) 等攻擊。

了解 CORS 之後就來在 BFF Gateway 的 Program.cs 內加入:

//...

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAll", builder =>
    {
        builder.AllowAnyOrigin()
               .AllowAnyMethod()
               .AllowAnyHeader();
    });
});

//...

app.UseCors("AllowAll"); // 加入這個

//...

結語

在這篇文章中,我們成功將多個 GraphQL 服務整合到單一入口點,並實現了 GraphQL Gateway 的 Stitch 功能。這次的實作採用了集中式架構 (Centralized),所有的設定與配置都在 Gateway 端完成。這種架構簡化了多服務的整合過程,讓前端能通過單一查詢點獲取完整的資料。不過,GraphQL Gateway 還有其他架構模式,例如聯邦式架構 (Federation),能將不同服務的邏輯分散處理,進一步提升系統的彈性與擴展性。讀者若有興趣,建議深入研究聯邦式架構,進一步探索 GraphQL Gateway 的更多應用場景。

最後,做完了所有 Backend,現在的微服務架構圖會變成這樣:

https://ithelp.ithome.com.tw/upload/images/20241011/20168953PXxwqTKrmQ.png


上一篇
Day 26 - 打造靈活高效的 GraphQL 服務:從設計到實作
下一篇
Day 28 - Cursor AI 魔法:一鍵打造現代化 To-Do List 網頁應用
系列文
DDD? Clean Architecture? Microservices? 帶你用.NET實作打造一個現代化微服務!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言